{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import os\n",
    "os.chdir('../')\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 简介\n",
    "上一讲我们实现了一个简单二元分类器：LogisticRegression，但通常情况下，我们面对的更多是多分类器的问题，而二分类转多分类的通常做法也很朴素，一般分为两种：**one-vs-rest**以及**one-vs-one**。顾名思义，one-vs-rest将多类别中的其中一类作为正类，剩余其他所有类别作为负类，对于`n_class`类别的分类问题，需要构建$n\\_class$种分类器；而one-vs-one是指进行两两分类，这样将会构造$n\\_class*(n\\_class-1)/2$种分类器，由于实现思路很简单，就直接贴出代码，将多分类实现封装到`MultiClassWrapper`类，并放到`ml_models.wrapper_models`包"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from ml_models.linear_model import *\n",
    "from ml_models.wrapper_models import *"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "#准备手写数据\n",
    "from sklearn.metrics import f1_score\n",
    "from sklearn import model_selection\n",
    "from sklearn import datasets\n",
    "digits = datasets.load_digits()\n",
    "data = digits['data']\n",
    "target = digits['target']\n",
    "X_train, X_test, y_train, y_test = model_selection.train_test_split(data, target, test_size=0.3,\n",
    "                                                                    random_state=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "#构建初始模型\n",
    "lr = LogisticRegression()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ovr: 0.9492701335705958\n"
     ]
    }
   ],
   "source": [
    "#进行one-vs-rest训练并评估\n",
    "ovr = MultiClassWrapper(lr, mode='ovr')\n",
    "ovr.fit(X_train, y_train)\n",
    "\n",
    "y = ovr.predict(X_test)\n",
    "print('ovr:', f1_score(y_test, y, average='macro'))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ovo: 0.959902103714483\n"
     ]
    }
   ],
   "source": [
    "#进行one-vs-one训练并评估\n",
    "ovo = MultiClassWrapper(lr, mode='ovo')\n",
    "ovo.fit(X_train, y_train)\n",
    "\n",
    "y = ovo.predict(X_test)\n",
    "print('ovo:', f1_score(y_test, y, average='macro'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `MultiClassWrapper`类实现细节"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "import threading\n",
    "import copy\n",
    "import numpy as np\n",
    "\n",
    "\"\"\"\n",
    "继承Thread,获取函数的返回值\n",
    "\"\"\"\n",
    "\n",
    "\n",
    "class MyThread(threading.Thread):\n",
    "    def __init__(self, target, args, kwargs, name=''):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.name = name\n",
    "        self.target = target\n",
    "        self.args = args\n",
    "        self.kwargs = kwargs\n",
    "        self.result = self.target(*self.args, **self.kwargs)\n",
    "\n",
    "    def get_result(self):\n",
    "        try:\n",
    "            return self.result\n",
    "        except:\n",
    "            return None\n",
    "\n",
    "\n",
    "class MultiClassWrapper(object):\n",
    "    def __init__(self, base_classifier, mode='ovr'):\n",
    "        \"\"\"\n",
    "        :param base_classifier: 实例化后的分类器\n",
    "        :param mode: 'ovr'表示one-vs-rest方式,'ovo'表示one-vs-one方式\n",
    "        \"\"\"\n",
    "        self.base_classifier = base_classifier\n",
    "        self.mode = mode\n",
    "\n",
    "    @staticmethod\n",
    "    def fit_base_classifier(base_classifier, x, y, **kwargs):\n",
    "        base_classifier.fit(x, y, **kwargs)\n",
    "\n",
    "    @staticmethod\n",
    "    def predict_proba_base_classifier(base_classifier, x):\n",
    "        return base_classifier.predict_proba(x)\n",
    "\n",
    "    def fit(self, x, y, **kwargs):\n",
    "        # 对y分组并行fit\n",
    "        self.n_class = np.max(y)\n",
    "        if self.mode == 'ovr':\n",
    "            # 打包数据\n",
    "            self.classifiers = []\n",
    "\n",
    "            for cls in range(0, self.n_class + 1):\n",
    "                self.classifiers.append(copy.deepcopy(self.base_classifier))\n",
    "            # 并行训练\n",
    "            tasks = []\n",
    "            for cls in range(len(self.classifiers)):\n",
    "                task = MyThread(target=self.fit_base_classifier,\n",
    "                                args=(self.classifiers[cls], x, (y == cls).astype('int')), kwargs=kwargs)\n",
    "                task.start()\n",
    "                tasks.append(task)\n",
    "            for task in tasks:\n",
    "                task.join()\n",
    "        elif self.mode == \"ovo\":\n",
    "            # 打包数据\n",
    "            self.classifiers = {}\n",
    "            for first_cls in range(0, self.n_class):\n",
    "                for second_cls in range(first_cls + 1, self.n_class + 1):\n",
    "                    self.classifiers[(first_cls, second_cls)] = copy.deepcopy(self.base_classifier)\n",
    "            # 并行训练\n",
    "            tasks = {}\n",
    "            for first_cls in range(0, self.n_class):\n",
    "                for second_cls in range(first_cls + 1, self.n_class + 1):\n",
    "                    index = np.where(y == first_cls)[0].tolist() + np.where(y == second_cls)[0].tolist()\n",
    "                    new_x = x[index, :]\n",
    "                    new_y = y[index]\n",
    "                    task = MyThread(target=self.fit_base_classifier,\n",
    "                                    args=(self.classifiers[(first_cls, second_cls)], new_x,\n",
    "                                          (new_y == first_cls).astype('int')), kwargs=kwargs)\n",
    "                    task.start()\n",
    "                    tasks[(first_cls, second_cls)] = task\n",
    "            for first_cls in range(0, self.n_class):\n",
    "                for second_cls in range(first_cls + 1, self.n_class + 1):\n",
    "                    tasks[(first_cls, second_cls)].join()\n",
    "\n",
    "    def predict_proba(self, x, **kwargs):\n",
    "        if self.mode == 'ovr':\n",
    "            tasks = []\n",
    "            probas = []\n",
    "            for cls in range(len(self.classifiers)):\n",
    "                task = MyThread(target=self.predict_proba_base_classifier, args=(self.classifiers[cls], x),\n",
    "                                kwargs=kwargs)\n",
    "                task.start()\n",
    "                tasks.append(task)\n",
    "            for task in tasks:\n",
    "                task.join()\n",
    "            for task in tasks:\n",
    "                probas.append(task.get_result())\n",
    "            total_probas = np.concatenate(probas, axis=1)\n",
    "            # 归一化\n",
    "            return total_probas / total_probas.sum(axis=1, keepdims=True)\n",
    "        elif self.mode == 'ovo':\n",
    "            tasks = {}\n",
    "            probas = {}\n",
    "            for first_cls in range(0, self.n_class):\n",
    "                for second_cls in range(first_cls + 1, self.n_class + 1):\n",
    "                    task = MyThread(target=self.predict_proba_base_classifier,\n",
    "                                    args=(self.classifiers[(first_cls, second_cls)], x), kwargs=kwargs)\n",
    "                    task.start()\n",
    "                    tasks[(first_cls, second_cls)] = task\n",
    "            for first_cls in range(0, self.n_class):\n",
    "                for second_cls in range(first_cls + 1, self.n_class + 1):\n",
    "                    tasks[(first_cls, second_cls)].join()\n",
    "            for first_cls in range(0, self.n_class):\n",
    "                for second_cls in range(first_cls + 1, self.n_class + 1):\n",
    "                    probas[(first_cls, second_cls)] = tasks[(first_cls, second_cls)].get_result()\n",
    "                    probas[(second_cls, first_cls)] = 1.0 - probas[(first_cls, second_cls)]\n",
    "            # 统计概率\n",
    "            total_probas = []\n",
    "            for first_cls in range(0, self.n_class + 1):\n",
    "                temp = []\n",
    "                for second_cls in range(0, self.n_class + 1):\n",
    "                    if first_cls != second_cls:\n",
    "                        temp.append(probas[(first_cls, second_cls)])\n",
    "                temp = np.concatenate(temp, axis=1).sum(axis=1, keepdims=True)\n",
    "                total_probas.append(temp)\n",
    "            # 归一化\n",
    "            total_probas = np.concatenate(total_probas, axis=1)\n",
    "            return total_probas / total_probas.sum(axis=1, keepdims=True)\n",
    "\n",
    "    def predict(self, x):\n",
    "        return np.argmax(self.predict_proba(x), axis=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
